Дизайнеры предложил провести замену шрифтов в мобильном приложении стартапа, который продает продукты питания.
Отдел продаж сомневается в необходимости предложенных изменений.
По итогу, проведён A/A/B-эксперимент. Пользователи приложения были разбиты на 3 группы: 2 контрольные (246, 247) со старыми шрифтами и одну экспериментальную (248) — с новыми.
По итогам проведенного тестирования был загружен лог с данными
Каждая запись в логе — это действие пользователя, или событие.
Загрузим необходимые для работы библиотеки
import pandas as pd
import datetime as dt
import numpy as np
import matplotlib.pyplot as plt
from pandas.plotting import register_matplotlib_converters
import warnings
import scipy.stats as stats
from scipy import stats as st
import seaborn as sns
import plotly.express as px
from plotly import graph_objects as go
import math as mth
pd.options.mode.chained_assignment = None
data=pd.read_csv('/Users/User/Downloads/logs_exp.csv', delim_whitespace=True )
data.head(10)
| EventName | DeviceIDHash | EventTimestamp | ExpId | |
|---|---|---|---|---|
| 0 | MainScreenAppear | 4575588528974610257 | 1564029816 | 246 |
| 1 | MainScreenAppear | 7416695313311560658 | 1564053102 | 246 |
| 2 | PaymentScreenSuccessful | 3518123091307005509 | 1564054127 | 248 |
| 3 | CartScreenAppear | 3518123091307005509 | 1564054127 | 248 |
| 4 | PaymentScreenSuccessful | 6217807653094995999 | 1564055322 | 248 |
| 5 | CartScreenAppear | 6217807653094995999 | 1564055323 | 248 |
| 6 | OffersScreenAppear | 8351860793733343758 | 1564066242 | 246 |
| 7 | MainScreenAppear | 5682100281902512875 | 1564085677 | 246 |
| 8 | MainScreenAppear | 1850981295691852772 | 1564086702 | 247 |
| 9 | MainScreenAppear | 5407636962369102641 | 1564112112 | 246 |
data= data.rename(columns={'EventName':'event_name', 'DeviceIDHash':'user_id',
'EventTimestamp':'event_time_stamp', 'ExpId':'group' })
data['date_time'] = pd.to_datetime(data['event_time_stamp'], unit='s')
data['date'] = data['date_time'].dt.strftime('%Y-%m-%d')
data.head(10)
| event_name | user_id | event_time_stamp | group | date_time | date | |
|---|---|---|---|---|---|---|
| 0 | MainScreenAppear | 4575588528974610257 | 1564029816 | 246 | 2019-07-25 04:43:36 | 2019-07-25 |
| 1 | MainScreenAppear | 7416695313311560658 | 1564053102 | 246 | 2019-07-25 11:11:42 | 2019-07-25 |
| 2 | PaymentScreenSuccessful | 3518123091307005509 | 1564054127 | 248 | 2019-07-25 11:28:47 | 2019-07-25 |
| 3 | CartScreenAppear | 3518123091307005509 | 1564054127 | 248 | 2019-07-25 11:28:47 | 2019-07-25 |
| 4 | PaymentScreenSuccessful | 6217807653094995999 | 1564055322 | 248 | 2019-07-25 11:48:42 | 2019-07-25 |
| 5 | CartScreenAppear | 6217807653094995999 | 1564055323 | 248 | 2019-07-25 11:48:43 | 2019-07-25 |
| 6 | OffersScreenAppear | 8351860793733343758 | 1564066242 | 246 | 2019-07-25 14:50:42 | 2019-07-25 |
| 7 | MainScreenAppear | 5682100281902512875 | 1564085677 | 246 | 2019-07-25 20:14:37 | 2019-07-25 |
| 8 | MainScreenAppear | 1850981295691852772 | 1564086702 | 247 | 2019-07-25 20:31:42 | 2019-07-25 |
| 9 | MainScreenAppear | 5407636962369102641 | 1564112112 | 246 | 2019-07-26 03:35:12 | 2019-07-26 |
data.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 244126 entries, 0 to 244125 Data columns (total 6 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 event_name 244126 non-null object 1 user_id 244126 non-null int64 2 event_time_stamp 244126 non-null int64 3 group 244126 non-null int64 4 date_time 244126 non-null datetime64[ns] 5 date 244126 non-null object dtypes: datetime64[ns](1), int64(3), object(2) memory usage: 11.2+ MB
print('В датафрейме', data.duplicated().sum(), 'дубликатов')
В датафрейме 413 дубликатов
data = data.drop_duplicates().reset_index(drop=True)
data.isna().sum()
event_name 0 user_id 0 event_time_stamp 0 group 0 date_time 0 date 0 dtype: int64
data.head(10)
| event_name | user_id | event_time_stamp | group | date_time | date | |
|---|---|---|---|---|---|---|
| 0 | MainScreenAppear | 4575588528974610257 | 1564029816 | 246 | 2019-07-25 04:43:36 | 2019-07-25 |
| 1 | MainScreenAppear | 7416695313311560658 | 1564053102 | 246 | 2019-07-25 11:11:42 | 2019-07-25 |
| 2 | PaymentScreenSuccessful | 3518123091307005509 | 1564054127 | 248 | 2019-07-25 11:28:47 | 2019-07-25 |
| 3 | CartScreenAppear | 3518123091307005509 | 1564054127 | 248 | 2019-07-25 11:28:47 | 2019-07-25 |
| 4 | PaymentScreenSuccessful | 6217807653094995999 | 1564055322 | 248 | 2019-07-25 11:48:42 | 2019-07-25 |
| 5 | CartScreenAppear | 6217807653094995999 | 1564055323 | 248 | 2019-07-25 11:48:43 | 2019-07-25 |
| 6 | OffersScreenAppear | 8351860793733343758 | 1564066242 | 246 | 2019-07-25 14:50:42 | 2019-07-25 |
| 7 | MainScreenAppear | 5682100281902512875 | 1564085677 | 246 | 2019-07-25 20:14:37 | 2019-07-25 |
| 8 | MainScreenAppear | 1850981295691852772 | 1564086702 | 247 | 2019-07-25 20:31:42 | 2019-07-25 |
| 9 | MainScreenAppear | 5407636962369102641 | 1564112112 | 246 | 2019-07-26 03:35:12 | 2019-07-26 |
Проверим на возможность попадания одного и того же пользователя в разные группы. Это необходимо для того, чтобы результаты A/B теста были корректны.
res=[]
for k in (data['group'].unique()):
list1= data.loc[data["group"]==k, "user_id"].unique()
for t in (data['group'].unique()):
list2=data.loc[data["group"]==t, "user_id"].unique()
if k!=t:
res=set(list1).intersection(list2)
print(res)
set()
Так как список set вывелся пустым, то можем сделать вывод, что персечений пользователей между группами нет
Промежуточный вывод
Изучим представленную информацию
print('Всего в логе отображено', data['event_time_stamp'].count(), 'событий')
print('В эксперименте приняло участие', data['user_id'].nunique(), 'пользователь')
print('Данные отражены за период с', min(data['date']), 'по', max(data['date']))
print('В среднем на одного пользователя приходится', round(data['event_time_stamp'].count()/data['user_id'].nunique(), 2),
'события. Рассмотрим, как распределены пользователи в разрезе событий (столбец "event_name") ')
Всего в логе отображено 243713 событий В эксперименте приняло участие 7551 пользователь Данные отражены за период с 2019-07-25 по 2019-08-07 В среднем на одного пользователя приходится 32.28 события. Рассмотрим, как распределены пользователи в разрезе событий (столбец "event_name")
Проанализируем, как меняется количество данных. Для этого пострим диаграмму в разрезе событий - это даст нам понимание, насколько полные данные есть по датам
plt.figure(figsize=(20, 10))
ax = sns.countplot(y=data['date'], hue='event_name', data=data)
ax.set_title('Количество различных событий в зависимости от времени в разрезе событий')
plt.show()
На графике видно, что часть данных в логах представленно неполно. Это связано с тем, что по некоторым пользователям события могут "доезжать" позже.
При проведении анализа, такие полупустые дни могут существенно искажать получаемую информацию по эксперименту, поэтому сделаем срез, изменив временной интервал, а так же рассчитаем, какое количество событий мы удалили из выборки, и как это может повлиять на проводимый анализ
data_loc = data.query('date >= "2019-08-01"')
print('Всего в логе отображено', data_loc['event_time_stamp'].count(), 'событий')
print('В эксперименте приняло участие', data_loc['user_id'].nunique(), 'пользователь')
print('Новые данные отражены за период с', min(data_loc['date']), 'по', max(data_loc['date']))
print('В среднем на одного пользователя приходится', round(data_loc['event_time_stamp'].count()/data_loc['user_id'].nunique(), 2),
'события.')
print('Было убрано', data['event_time_stamp'].count()-data_loc['event_time_stamp'].count(),
'событий, прошедших ранее 01.08.2019б что составляет',
round((data['event_time_stamp'].count()-data_loc['event_time_stamp'].count())/data['event_time_stamp'].count()*100,2),
'% от первоначального количества.' )
print('Были удалены', data['user_id'].nunique()-data_loc['user_id'].nunique(),
'пользователей, прошедших ранее 01.08.2019б что составляет',
round((data['user_id'].nunique()-data_loc['user_id'].nunique())/data['user_id'].nunique()*100,2),
'% от первоначального количества.' )
data_loc
Всего в логе отображено 240887 событий В эксперименте приняло участие 7534 пользователь Новые данные отражены за период с 2019-08-01 по 2019-08-07 В среднем на одного пользователя приходится 31.97 события. Было убрано 2826 событий, прошедших ранее 01.08.2019б что составляет 1.16 % от первоначального количества. Были удалены 17 пользователей, прошедших ранее 01.08.2019б что составляет 0.23 % от первоначального количества.
| event_name | user_id | event_time_stamp | group | date_time | date | |
|---|---|---|---|---|---|---|
| 2826 | Tutorial | 3737462046622621720 | 1564618048 | 246 | 2019-08-01 00:07:28 | 2019-08-01 |
| 2827 | MainScreenAppear | 3737462046622621720 | 1564618080 | 246 | 2019-08-01 00:08:00 | 2019-08-01 |
| 2828 | MainScreenAppear | 3737462046622621720 | 1564618135 | 246 | 2019-08-01 00:08:55 | 2019-08-01 |
| 2829 | OffersScreenAppear | 3737462046622621720 | 1564618138 | 246 | 2019-08-01 00:08:58 | 2019-08-01 |
| 2830 | MainScreenAppear | 1433840883824088890 | 1564618139 | 247 | 2019-08-01 00:08:59 | 2019-08-01 |
| ... | ... | ... | ... | ... | ... | ... |
| 243708 | MainScreenAppear | 4599628364049201812 | 1565212345 | 247 | 2019-08-07 21:12:25 | 2019-08-07 |
| 243709 | MainScreenAppear | 5849806612437486590 | 1565212439 | 246 | 2019-08-07 21:13:59 | 2019-08-07 |
| 243710 | MainScreenAppear | 5746969938801999050 | 1565212483 | 246 | 2019-08-07 21:14:43 | 2019-08-07 |
| 243711 | MainScreenAppear | 5746969938801999050 | 1565212498 | 246 | 2019-08-07 21:14:58 | 2019-08-07 |
| 243712 | OffersScreenAppear | 5746969938801999050 | 1565212517 | 246 | 2019-08-07 21:15:17 | 2019-08-07 |
240887 rows × 6 columns
NB Убрали 1,16% событий и 0,23% уникальных пользователей от первоначального количества - что не повлияет на корректные результаты анализа эксперимента.
Убедимся, что в новом датафрейме есть пользователи из всех трех групп
plt.figure(figsize=(15, 10))
ax = sns.countplot(y=data_loc['date'], hue='group', data=data_loc)
ax.set_title('Количество различных событий в зависимости от времени в разрезе групп')
plt.show()
groups_user= data_loc.groupby('group').agg({'user_id': ['count', 'nunique']})
groups_user
| user_id | ||
|---|---|---|
| count | nunique | |
| group | ||
| 246 | 79302 | 2484 |
| 247 | 77022 | 2513 |
| 248 | 84563 | 2537 |
Проанализируем, какие события есть в логах, как часто они встречаются, так же отсортируем события по частоте
event_name_log= (data_loc.groupby('event_name')['user_id']
.agg(['count'])
.rename(columns={'count':'event_count'})
.sort_values(by='event_count', ascending=False)
.reset_index())
event_name_log
| event_name | event_count | |
|---|---|---|
| 0 | MainScreenAppear | 117328 |
| 1 | OffersScreenAppear | 46333 |
| 2 | CartScreenAppear | 42303 |
| 3 | PaymentScreenSuccessful | 33918 |
| 4 | Tutorial | 1005 |
Промежуточный вывод
Отсортируем события по числу пользователей, определим долю пользователей, которые хоть раз совершали событие
user_id_log =(data_loc.groupby('event_name')['user_id']
.agg(['nunique'])
.rename(columns={'nunique':'event_user'})
.sort_values(by='event_user', ascending=False)
.reset_index())
user_id_log['prop_user']= round(user_id_log['event_user']/ data_loc['user_id'].nunique()*100,2)
user_id_log
| event_name | event_user | prop_user | |
|---|---|---|---|
| 0 | MainScreenAppear | 7419 | 98.47 |
| 1 | OffersScreenAppear | 4593 | 60.96 |
| 2 | CartScreenAppear | 3734 | 49.56 |
| 3 | PaymentScreenSuccessful | 3539 | 46.97 |
| 4 | Tutorial | 840 | 11.15 |
Для удобства работы, сгруппируем данные по событиям и группам:
groups_user= data_loc.groupby('group').agg({'user_id': 'nunique'})
groups_user = pd.concat([groups_user], ignore_index=True)
groups_user #вспомогательная таблица, нужна для того, чтобы понимать сколько пользователей находится в каждой группе
| user_id | |
|---|---|
| 0 | 2484 |
| 1 | 2513 |
| 2 | 2537 |
user_id_log24=user_id_log
groups=[246, 247, 248]
user_id_groups =[]
i=0
for group in groups:
user_id_groups = data_loc[data_loc['group']==group]
user_id_log_groups =(user_id_groups.groupby('event_name')['user_id']
.agg(['nunique'])
.rename(columns={'nunique':'event_user'})
.sort_values(by='event_user', ascending=False)
.reset_index())
user_id_log_groups['prop_user']=round(user_id_log_groups['event_user']/ (groups_user['user_id'][i])*100,2)
user_id_log_groups=user_id_log_groups.rename(columns={'event_user':'event_'+str(group), 'prop_user':'prop_'+str(group)})
user_id_log24=user_id_log24.merge(user_id_log_groups, on='event_name')
i=i+1
user_id_log24
| event_name | event_user | prop_user | event_246 | prop_246 | event_247 | prop_247 | event_248 | prop_248 | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | MainScreenAppear | 7419 | 98.47 | 2450 | 98.63 | 2476 | 98.53 | 2493 | 98.27 |
| 1 | OffersScreenAppear | 4593 | 60.96 | 1542 | 62.08 | 1520 | 60.49 | 1531 | 60.35 |
| 2 | CartScreenAppear | 3734 | 49.56 | 1266 | 50.97 | 1238 | 49.26 | 1230 | 48.48 |
| 3 | PaymentScreenSuccessful | 3539 | 46.97 | 1200 | 48.31 | 1158 | 46.08 | 1181 | 46.55 |
| 4 | Tutorial | 840 | 11.15 | 278 | 11.19 | 283 | 11.26 | 279 | 11.00 |
В полученной таблице
Рассмотрим получившуюся воронку, для того чтобы наглядно увидеть последовательность действий, совершаемых пользователем.
NB К странице Tutorial - помощи, для совершения покупки - незначительное количество обращений пользователей, поэтому рассмотрим события далее без анализа действия Tutorial.
Сначала по общему количеству пользователей:
user_id_log24=user_id_log24[user_id_log24['event_name'] != 'Tutorial' ] #убираем Tutorial
fig=go.Figure()
fig.add_trace(go.Funnel(
x=user_id_log24['prop_user'],
y=user_id_log24['event_name']))
fig.update_layout(title={'text': "Воронка событий"})
fig.show()
Исходя из полученных данных, можно предположить, что события проходят в следующем порядке: MainScreenAppear -> OffersScreenAppear -> CartScreenAppear -> PaymentScreenSuccessful
Действительно, пользователь сначал заходит на главную страницу -> рассматривает объявление -> кладет его в корзину -> оплачивает сформированный заказ.
NB. Если исходить из того, как определяется последовательность действий, то разумно предположить, что все уникальные посетители должны начинать работу с посещения главной страницы. Однако мы видим, что таких посетителей 98,47%. Примерно 1,5% посетителей попадают в приложение не через стартовую страницу.
Возможные варианты таких действий пользователей:
Первое, второе и третье предположение - к маркетинговому отделу, четвертое - к разработчикам.
Поскольку количество таких "аномальных" посещений не превышает 1,5%, то можем ими пренебречь в рамках проводимого исследования
Рассмотрим, как порядок событий происходит в разбивке по группам
groups=[246, 247, 248]
fig=go.Figure()
for group in groups:
fig.add_trace(go.Funnel(
name = str(group),
x=user_id_log24['prop_'+str(group)],
y=user_id_log24['event_name']
))
fig.update_layout(title={'text': "Воронка событий по группам"})
fig.show()
Видим, что внутри групп последовательность выполнения действий та же: MainScreenAppear -> OffersScreenAppear -> CartScreenAppear -> PaymentScreenSuccessful
До оплаты (PaymentScreenSuccessful) от первоначального количества пользователей в группе:
В среднем от общего количества пользователей доходит 46,97% полльзователей.
NB И так же, как и в целом для всего исследования, количество уникальных пользователей в первом действии не равно 100% количеству уникальных пользователей внутри группы!
Промежуточный вывод
Проанализируем теперь какое количество пользователей переходит к следующему шагу воронки от числа пользователей к предыдущему.
Для начала рассмотрим соотношение для общего количества
fig=go.Figure()
fig.add_trace(go.Funnel(
x=user_id_log24['prop_user'],
y=user_id_log24['event_name'],
textinfo = 'percent previous'))
fig.update_layout(title={'text': "Воронка событий"})
fig.show()
Больше всего теряется человек, при переходе с первого события MainScreenAppear на второе OffersScreenAppear - 38%.
Можно выдвинуть несколько версий:
С другой стороны можно отметить, что
Следовательно, товар интересен пользователю, и нужно уделить внимание первой странице, с которой начинается "путь пользователя". Возможно, как раз и стоит поработать с визуальным оформлением, проработать "путь пользователя" от начальной страницы до покупки
Рассмотрим отношение количества пользователей к предыдущему событию в разрезе групп
groups=[246, 247, 248]
fig=go.Figure()
for group in groups:
fig.add_trace(go.Funnel(
name = str(group),
x=user_id_log24['prop_'+str(group)],
y=user_id_log24['event_name'],
textinfo = 'percent previous'
))
fig.update_layout(title={'text': "Воронка событий по группам"})
fig.show()
Внутри групп наблюдаем ту же картину - резкое падение при переходе от первого события ко второму, и затем стабильная фиксация в районе 80-95% после добавления товара в корзину и его последущей оплате
Промежуточный вывод
Еще раз представим, сколько пользователей находится в каждой экспериментальной группе
user_id_log24
| event_name | event_user | prop_user | event_246 | prop_246 | event_247 | prop_247 | event_248 | prop_248 | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | MainScreenAppear | 7419 | 98.47 | 2450 | 98.63 | 2476 | 98.53 | 2493 | 98.27 |
| 1 | OffersScreenAppear | 4593 | 60.96 | 1542 | 62.08 | 1520 | 60.49 | 1531 | 60.35 |
| 2 | CartScreenAppear | 3734 | 49.56 | 1266 | 50.97 | 1238 | 49.26 | 1230 | 48.48 |
| 3 | PaymentScreenSuccessful | 3539 | 46.97 | 1200 | 48.31 | 1158 | 46.08 | 1181 | 46.55 |
Определим, какой статистический тест будем использовать в исследовании.
Следовательно, будем использовать z-тест. Причем, т.к. у нас две контрольных группы, то работаем с А/А/В - тестом.
Определим, что будет являться пололжительным результатом нашего исследования.
Напомню, что проведение исследования связано с предполагаемым изменением оформительского шрифта в приложении. Отдел продаж высказал опасение, что такое нововведение может снизить продажи товара через приложение, т.к. изменения оттолкнут пользователя. Отдел дизайна утверждает, что подобное изменение не сократит количество пользователей.
Исходя из этого критерием проверки гипотезы определим: в экспериментальной группе доля активных пользователей будет сопоставима с долей активных пользователей контрольных групп.
Поскольку изначально в поставленной задачи отсуствуют какие-либо количественные показатели изменений, то будем считать параметр alpha=0,05. Так же рассмотрим насколько различны результаты контрольных и эксперментальных групп при alpha=0,1.
Напишем функцию, позволяющую проводить проверку статистических гипотез. При проведении таких проверок скорректируем alpha=alpha/16 (поправка Бонферрони, т.к. мы проведём 4 эксперимента (246/247, 246/248, 247/248, 246+247/248 ) с 4 гипотезами
Для автоматизации проверки гипотез воспользуемся вспомогательной группой, в которой находится информация о номере группы, и количестве уникальных пользователей
groups_user= data_loc.groupby('group').agg({'group':'unique', 'user_id': 'nunique'})
groups_user = pd.concat([groups_user], ignore_index=True)
groups_user.group = groups_user.group.astype(int)
groups_user.loc[ len(groups_user.index )] = [(groups_user['group'][0]+ groups_user['group'][1]),
(groups_user['user_id'][0]+ groups_user['user_id'][1])]
groups_user
| group | user_id | |
|---|---|---|
| 0 | 246 | 2484 |
| 1 | 247 | 2513 |
| 2 | 248 | 2537 |
| 3 | 493 | 4997 |
Напишем функцию вычисления результатов тестирования
#first_group - номер первой группы,
#second_group - номер второй группы,
#groups_user_f - количество уникальных пользователей для первой группы, берется из вспомогательной таблицы,
#groups_user_s - количество уникальных пользователей для второй группы, берется из вспомогательной таблицы,
#alpha - alpha
def experiment_a_b(first_group, second_group, groups_user_f, groups_user_s, alpha):
print ('Для групп {}, {}'.format(first_group, second_group))
print('-----------------------------------')
for i in user_id_log24.index:
alpha = alpha/16
# пропоция успехов в первой группе:
p1 = user_id_log24['event_'+str(first_group)][i]/groups_user_f
# пропорция успехов во второй группе:
p2 = user_id_log24['event_'+str(second_group)][i]/groups_user_s
# разница пропорций в датасетах
difference = p1 - p2
p_combined = ((user_id_log24['event_'+str(first_group)][i] + user_id_log24['event_'+str(second_group)][i]) /
(groups_user_f + groups_user_s))
# считаем статистику в ст.отклонениях стандартного нормального распределения
z_value = difference / mth.sqrt(p_combined * (1 - p_combined) *
(1/groups_user_f + 1/groups_user_s))
# задаем стандартное нормальное распределение (среднее 0, ст.отклонение 1)
distr = st.norm(0, 1)
p_value = (1 - distr.cdf(abs(z_value))) * 2
print('{} p-значение: {}'.format(user_id_log24['event_name'][i], p_value))
if (p_value < alpha):
print("Отвергаем H0: между групами есть значимая разница")
else:
print("Не получилось отвергнуть H0, нет оснований считать группы разными")
print('')
Выдвинем для проверки статистической значимости две гипотезы:
Порогом статистической значимости установим alpha=0.05
Проверим выдвинутые гипотезы для контрольных групп 246, 247 при значении alpha=0.05
test_user_group=[[246,247]]
alpha=0.05
for i, pair in enumerate(test_user_group):
first_group=test_user_group[i][0]
second_group=test_user_group[i][1]
groups_user_f=int(groups_user.loc[groups_user['group']==first_group, 'user_id'])
groups_user_s=int(groups_user.loc[groups_user['group']==second_group, 'user_id'])
experiment_a_b(first_group, second_group, groups_user_f, groups_user_s, alpha)
Для групп 246, 247 ----------------------------------- MainScreenAppear p-значение: 0.7570597232046099 Не получилось отвергнуть H0, нет оснований считать группы разными OffersScreenAppear p-значение: 0.2480954578522181 Не получилось отвергнуть H0, нет оснований считать группы разными CartScreenAppear p-значение: 0.22883372237997213 Не получилось отвергнуть H0, нет оснований считать группы разными PaymentScreenSuccessful p-значение: 0.11456679313141849 Не получилось отвергнуть H0, нет оснований считать группы разными
Промежуточный вывод По итогам исследования различий между двумя контрольными группами (А/А-тест) - невыявленно, обе группы показали одинаковые результаты конверсии при использовании приложения. Так же ранее мы подтвердили, что в каждой группе находятся уникальные пользователи. Следовательно можно признать данные группы контрольными
Проведем анализ данных контрольных групп и экспериментальной при alpha=0.05
test_user_group=[[246,248], [247,248]]
alpha=0.05
for i, pair in enumerate(test_user_group):
first_group=test_user_group[i][0]
second_group=test_user_group[i][1]
groups_user_f=int(groups_user.loc[groups_user['group']==first_group, 'user_id'])
groups_user_s=int(groups_user.loc[groups_user['group']==second_group, 'user_id'])
experiment_a_b(first_group, second_group, groups_user_f, groups_user_s, alpha)
Для групп 246, 248 ----------------------------------- MainScreenAppear p-значение: 0.2949721933554552 Не получилось отвергнуть H0, нет оснований считать группы разными OffersScreenAppear p-значение: 0.20836205402738917 Не получилось отвергнуть H0, нет оснований считать группы разными CartScreenAppear p-значение: 0.07842923237520116 Не получилось отвергнуть H0, нет оснований считать группы разными PaymentScreenSuccessful p-значение: 0.2122553275697796 Не получилось отвергнуть H0, нет оснований считать группы разными Для групп 247, 248 ----------------------------------- MainScreenAppear p-значение: 0.4587053616621515 Не получилось отвергнуть H0, нет оснований считать группы разными OffersScreenAppear p-значение: 0.9197817830592261 Не получилось отвергнуть H0, нет оснований считать группы разными CartScreenAppear p-значение: 0.5786197879539783 Не получилось отвергнуть H0, нет оснований считать группы разными PaymentScreenSuccessful p-значение: 0.7373415053803964 Не получилось отвергнуть H0, нет оснований считать группы разными
Ни в одном из проведенных расчетов не получилось отвергнуть H0.
Проведем анализ для сводной группы 246+247, ранее мы её обозначили как группа 493
user_id_log24['event_493']=user_id_log24['event_246']+user_id_log24['event_247']
test_user_group=[[493,248]]
alpha=0.05
for i, pair in enumerate(test_user_group):
first_group=test_user_group[i][0]
second_group=test_user_group[i][1]
groups_user_f=int(groups_user.loc[groups_user['group']==first_group, 'user_id'])
groups_user_s=int(groups_user.loc[groups_user['group']==second_group, 'user_id'])
experiment_a_b(first_group, second_group, groups_user_f, groups_user_s, alpha)
Для групп 493, 248 ----------------------------------- MainScreenAppear p-значение: 0.29424526837179577 Не получилось отвергнуть H0, нет оснований считать группы разными OffersScreenAppear p-значение: 0.43425549655188256 Не получилось отвергнуть H0, нет оснований считать группы разными CartScreenAppear p-значение: 0.18175875284404386 Не получилось отвергнуть H0, нет оснований считать группы разными PaymentScreenSuccessful p-значение: 0.6004294282308704 Не получилось отвергнуть H0, нет оснований считать группы разными
Промежуточный вывод Проведенный A/B тест показал, что:
Проведем эксперимент при уровне значимости равной 0,1.
test_user_group=[[246,247],[246,248], [247,248], [493,248]]
alpha=0.1
for i, pair in enumerate(test_user_group):
first_group=test_user_group[i][0]
second_group=test_user_group[i][1]
groups_user_f=int(groups_user.loc[groups_user['group']==first_group, 'user_id'])
groups_user_s=int(groups_user.loc[groups_user['group']==second_group, 'user_id'])
experiment_a_b(first_group, second_group, groups_user_f, groups_user_s, alpha)
Для групп 246, 247 ----------------------------------- MainScreenAppear p-значение: 0.7570597232046099 Не получилось отвергнуть H0, нет оснований считать группы разными OffersScreenAppear p-значение: 0.2480954578522181 Не получилось отвергнуть H0, нет оснований считать группы разными CartScreenAppear p-значение: 0.22883372237997213 Не получилось отвергнуть H0, нет оснований считать группы разными PaymentScreenSuccessful p-значение: 0.11456679313141849 Не получилось отвергнуть H0, нет оснований считать группы разными Для групп 246, 248 ----------------------------------- MainScreenAppear p-значение: 0.2949721933554552 Не получилось отвергнуть H0, нет оснований считать группы разными OffersScreenAppear p-значение: 0.20836205402738917 Не получилось отвергнуть H0, нет оснований считать группы разными CartScreenAppear p-значение: 0.07842923237520116 Не получилось отвергнуть H0, нет оснований считать группы разными PaymentScreenSuccessful p-значение: 0.2122553275697796 Не получилось отвергнуть H0, нет оснований считать группы разными Для групп 247, 248 ----------------------------------- MainScreenAppear p-значение: 0.4587053616621515 Не получилось отвергнуть H0, нет оснований считать группы разными OffersScreenAppear p-значение: 0.9197817830592261 Не получилось отвергнуть H0, нет оснований считать группы разными CartScreenAppear p-значение: 0.5786197879539783 Не получилось отвергнуть H0, нет оснований считать группы разными PaymentScreenSuccessful p-значение: 0.7373415053803964 Не получилось отвергнуть H0, нет оснований считать группы разными Для групп 493, 248 ----------------------------------- MainScreenAppear p-значение: 0.29424526837179577 Не получилось отвергнуть H0, нет оснований считать группы разными OffersScreenAppear p-значение: 0.43425549655188256 Не получилось отвергнуть H0, нет оснований считать группы разными CartScreenAppear p-значение: 0.18175875284404386 Не получилось отвергнуть H0, нет оснований считать группы разными PaymentScreenSuccessful p-значение: 0.6004294282308704 Не получилось отвергнуть H0, нет оснований считать группы разными
Промежуточный вывод С учетом поправки Бонферрони, различий в группах ни в одном из 16 проведнных экспериментов не было выявленно
Отдел дизайна разработал новое оформление мобильного приложения (шрифт-схема), в течение двух недель с 25.07.2019 по 07.08.2019 проводилось тестирование в трех группах: контрольные группы (246, 247) с текущей версией приложения, и экспериментальной группой (248), у которой была установленна новая версия приложения с измененной шрифт-схемой. Действия пользователей записывались в лог-файл, который и был представлен для анализа.
Таким образом подтвердилась версия отдела дизайна, что изменение шрифта в приложении не приведет к оттоку пользователей.
С другой стороны возникает закономерный вопрос, что если такое изменение не принесло видимых результатов
Исходя из полученного анализа воронки продаж, необходимо сосредоточиться на вариантах сокращения потерь при пререходе пользователей с MainScreenAppear на OffersScreenAppear. Причиной потерь может быть как программная составляющая приложения, так и смысловое наполнение главной страницы.